/*
 *  Copyright (C) 2004 Cidero, Inc.
 *
 *  Permission is hereby granted to any person obtaining a copy of 
 *  this software to use, copy, modify, merge, publish, and distribute
 *  the software for any non-commercial purpose, subject to the
 *  following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY IN CONNECTION WITH THE SOFTWARE.
 * 
 *  File: $RCSfile: CDSObjectList.java,v $
 *
 */

package com.cidero.upnp;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;
import java.util.ArrayList;
import java.util.logging.Logger;
import java.util.Comparator;
import java.util.Collections;
import java.util.Iterator;
import java.util.HashMap;

import org.apache.xerces.parsers.DOMParser;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 *  Class to help manage lists of Content Directory Service objects
 *  
 */
public class CDSObjectList extends Vector
{
  private static Logger logger = Logger.getLogger("com.cidero.upnp");

  public CDSObjectList()
  {
  }

  /** 
   * Construct a list of ContentDirObject's from a file
   */
  public CDSObjectList( InputStream fileStream ) throws UPnPException
  {
    parseStream( fileStream );
  }

  /** 
   * Construct a list of ContentDirObject's from an XML DIDL-Lite string 
   *
   * @param  xmlString     DIDL-Lite XML string containing object metadata
   *                       ( including URL resource(s) )
   * @throws UPnPException if the XML parser encounters an error (badly
   *         formed XML)
   */
  public CDSObjectList( String xmlString ) throws UPnPException
  {

    /* Apache parser defaults to UTF-8 - trying ISO for grins... */  
    /*
    if( ! xmlString.startsWith("<?xml") )
    {
      xmlString = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>" + xmlString;
    }
    */
    
    try
    {
      byte[] xmlByteArray = xmlString.getBytes("UTF-8");
      ByteArrayInputStream xmlStream = new  ByteArrayInputStream( xmlByteArray );
      parseStream( xmlStream );
    }
    catch( UPnPException e )
    {
      // Modify the exception to print out the whole XML string in this case
      throw new UPnPException("Error parsing XML string: " + xmlString + 
                              "\n Details: " + e );
    }
    catch( UnsupportedEncodingException e )
    {
      throw new UPnPException("Error parsing XML string: " + xmlString + 
                              "\n Details: " + e );
    }
  }

  public Object clone() 
  {
    CDSObjectList objList = new CDSObjectList();

    for( int n = 0 ; n < size() ; n++ )
    {
      CDSObject obj = (CDSObject)(getObject(n).clone());
      objList.add( obj );
    }

    return objList;
  }

  /** 
   * parse an XML/DIDL input stream (using DOM), and fill the list with
   * the parsed UPNP objects
   *
   * @param     Input stream
   * @throws    UPnPException if the XML parser encounters an error (badly
   *            formed XML)
   */ 
  public void parseStream( InputStream inputStream )  throws UPnPException
  {
    DOMParser parser = new DOMParser();

    try 
    {
      InputSource inputSource = new InputSource( inputStream );
      inputSource.setEncoding( "UTF-8" );
      parser.parse( inputSource );
      Document doc = parser.getDocument();
      DOMToCDS( doc );
    }
    catch( IOException e )
    {
      throw new UPnPException( "Error parsing XML: " + e.getMessage() );
    }
    catch( SAXException e ) 
    {
      throw new UPnPException( "Error parsing XML: " + e.getMessage() );
    }
  }

  /** 
   * Construct a list of ContentDirObject's from a DOM document tree
   * Currently assumes that document has 'single level' of container/items
   * which *should* be true for the UPNP content directory service 
   * responses received from an A/V server (TODO - check this is true - OJN)
   */  
  public CDSObjectList( Node node )
  {
    DOMToCDS( node );
  }

  public CDSObject getObject(int n)
  {
    Object obj = null;
    try
    {
      obj = get(n);
    }
    catch (Exception e)
    {
    };

    return (CDSObject)obj;
  }

  public void DOMToCDS( Node node )
  {
    switch( node.getNodeType() )
    {
      case Node.DOCUMENT_NODE:
        //System.out.println("<xml version = \"1.0\">\n");
        Document doc = (Document)node;
        DOMToCDS( doc.getDocumentElement() );
        break;

      case Node.ELEMENT_NODE:

        //
        // This is the DIDL-Lite node - Process all children containers and/or
        // items (non recursively)
        //
        //System.out.println("DOMToCDS:Element node: " + node.getNodeName() );
        DOMContainersAndItemsToCDS( node.getChildNodes() );
        break;
    }
  }
  
  private void DOMContainersAndItemsToCDS( NodeList nodeList )
  {
    //System.out.println("N child nodes: " + nodeList.getLength() );

    for( int n = 0 ; n < nodeList.getLength() ; n++ )
    {
      Node node = nodeList.item(n);
      
      //System.out.println("child node: " + node.getNodeName() +
      //                         "value: [" + node.getNodeValue() + "]" );

      // Skip over non-element nodes
      if( node.getNodeType() != Node.ELEMENT_NODE )
      {
        //        System.out.println("Not an element - skipping\n");
        continue;
      }
      
      
      Element element = (Element)node;
      
      NodeList classElementList = element.getElementsByTagName( "upnp:class" );

      if( classElementList.getLength() != 1 )
      {
        System.out.println("Error - element has no upnp class tag\n");
        continue;
      }
      
      //System.out.println("Found matching upnp:class element\n");

      // Should be single text sub-node containing class name

      NodeList textNodeList = classElementList.item(0).getChildNodes();
      
      if( textNodeList.getLength() != 1 )
      {
        System.out.println("Error - upnp class element childNodes != 1\n");
        continue;
      }

      try
      {
        String upnpClass = textNodeList.item(0).getNodeValue();

        // Instantiate object of the appropriate type and initialize it
        // with all the data in the DOM version of the UPNP container/item 

        //System.out.println("upnpClass is: " + upnpClass );

        CDSObject obj = CreateCDSObject( upnpClass, node );
        add( obj );
      }
      catch( DOMException e )
      {
        System.out.println("UPNP class not found" + e.getMessage() );
      }

    }

  }
  
  private CDSObject CreateCDSObject( String upnpClass,
                                     Node node )
  {
    if( upnpClass.indexOf( "object.item.videoItem.movie" ) >= 0 )
    {
      //
      // Call form of constructor that takes a DOM node
      // DOM node gets passed down constructor chain all the way to
      // the base class.
      //
      CDSMovie movie = new CDSMovie( node );
      return movie;
    }
    else if( upnpClass.indexOf( "object.item.videoItem" ) >= 0 )
    {
      CDSVideoItem videoItem = new CDSVideoItem( node );
      return videoItem;
    }
    else if( upnpClass.indexOf( "object.item.imageItem.photo" ) >= 0 )
    {
      CDSPhoto photo = new CDSPhoto( node );
      return photo;
    }
    else if( upnpClass.indexOf( "object.item.imageItem" ) >= 0 )
    {
      CDSImageItem imageItem = new CDSImageItem( node );
      return imageItem;
    }
    else if( upnpClass.indexOf( "object.item.audioItem.musicTrack" ) >= 0 )
    {
      CDSMusicTrack musicTrack = new CDSMusicTrack( node );
      return musicTrack;
    }
    else if( upnpClass.indexOf( "object.item.audioItem.audioBroadcast" ) >= 0 )
    {
      CDSAudioBroadcast audioBroadcast = new CDSAudioBroadcast( node );
      return audioBroadcast;
    }
    else if( upnpClass.indexOf( "object.item.audioItem" ) >= 0 )
    {
      CDSAudioItem audioItem = new CDSAudioItem( node );
      return audioItem;
    }
    else if( upnpClass.indexOf( "object.item.playlistItem" ) >= 0 )
    {
      CDSPlaylistItem playlistItem = new CDSPlaylistItem( node );
      return playlistItem;
    }
    else if( upnpClass.indexOf( "object.item" ) >= 0 )
    {
      CDSItem item = new CDSItem( node );
      return item;
    }
    else if( upnpClass.indexOf( "object.container.album.musicAlbum" ) >= 0 )
    {
      CDSMusicAlbum obj = new CDSMusicAlbum( node );
      return obj;
    }
    else if( upnpClass.indexOf( "object.container.album" ) >= 0 )
    {
      CDSAlbum obj = new CDSAlbum( node );
      return obj;
    }
    else if( upnpClass.indexOf( "object.container.playlistContainer" ) >= 0 )
    {
      CDSPlaylistContainer obj = new CDSPlaylistContainer( node );
      return obj;
    }
    else if( upnpClass.indexOf( "object.container.storageFolder" ) >= 0 )
    {
      CDSStorageFolder obj = new CDSStorageFolder( node );
      return obj;
    }
    else if( upnpClass.indexOf( "object.container" ) >= 0 )
    {
      CDSContainer obj = new CDSContainer( node );
      return obj;
    }
    else
    {
      System.out.println("Unsupported UPNP object type '" + upnpClass + "'" );
    }

    return null;
  }

  /**
   *  Create a new version of the object list with only the best single
   *  matching resource for each object
   *
   *  TODO: Currently, only low-level CDSObject creator/title properties are 
   *  copied to new CDSObjects - implement full cloning scheme for CDS
   *  heirarchy when time allows
   */
  public CDSObjectList getBestMatchingResource( ArrayList protocolInfoList,
                                                int losslessWMATranscodeThresh)
  {
    CDSObjectList objList = new CDSObjectList();

    for( int n = 0 ; n < size() ; n++ )
    {
      CDSObject obj = (CDSObject)get(n);
      
      CDSResource bestMatchResource = 
      obj.getBestMatchingResource( protocolInfoList, null, null,
                                   losslessWMATranscodeThresh );
      
      if( bestMatchResource == null )
        continue;
      
      CDSItem bestMatchObj = new CDSItem();

      bestMatchObj.setTitle( obj.getTitle() );
      bestMatchObj.setCreator( obj.getCreator() );

      // If no creator property in object, try artist as backup 
      if( bestMatchObj.getCreator() == null ) 
        bestMatchObj.setCreator( obj.getArtist() );
      
      bestMatchObj.addResource( bestMatchResource );
      objList.add( bestMatchObj );
    }
    
    if( objList.size() > 0 )
      return objList;

    return null;
  }

  /**
   *  Sort the object list by the title property
   */
  public void sortByTitle()
  {
    Collections.sort( this, new TitleComparator() );
  }

  class TitleComparator implements Comparator
  {
    public int compare( Object obj1, Object obj2 )
    {
      String title1 = ((CDSObject)obj1).getTitle().toLowerCase();
      String title2 = ((CDSObject)obj2).getTitle().toLowerCase();
      return title1.compareTo(title2);
    }
  }
  
  /**
   *  Get count of container objects (as opposed to item objects)
   *  in object list
   */
  public int getContainerCount()
  {
    int count = 0;
    
    for( int n = 0 ; n < size() ; n++ )
    {
      CDSObject obj = (CDSObject)get(n);
      if( obj.isContainer() )
        count++;
    }
    return count;
  }

  /**
   *  Get count of item objects (as opposed to container objects)
   *  in object list
   */
  public int getItemCount()
  {
    int count = 0;
    
    for( int n = 0 ; n < size() ; n++ )
    {
      CDSObject obj = (CDSObject)get(n);
      if( obj.isItem() )
        count++;
    }
    return count;
  }
  
  /**
   *  Shuffle the list by album
   */
  public void shuffleByAlbum()
  {
    /* 
     * Make hash table of albums containers, adding tracks to 
     * containers as we go.  Then shuffle the hash table, and flatten
     * all the objects back to the list (in-place)
     */
    
    // Initial capacity = 2048 albums
    HashMap albumHashMap = new HashMap(2048);

    for( int n = 0 ; n < size() ; n++ )
    {
      CDSObject obj = (CDSObject)get(n);
      if( obj instanceof CDSMusicTrack )
      {
        CDSMusicTrack track = (CDSMusicTrack)obj;
        
        // Use combination of album and genre to help with album name
        // collisions. Note that soundtracks typically use the Genre
        // 'Soundtrack'
        String hashKey = track.getAlbum() + "," + track.getGenre();
        //System.out.println("track " + track.getTitle() );
        
        if( albumHashMap.containsKey(hashKey) )
        {
          CDSMusicAlbum albumObj = (CDSMusicAlbum)albumHashMap.get(hashKey);

          int origTrackNum = track.getOriginalTrackNum();
          if( origTrackNum == 0 )
          {
            // If no track num info avail, just add track to end of album
            albumObj.addChild( track );
          }
          else
          {
            // Insert track in proper place in album's track list
            CDSObjectList albumTrackList = albumObj.getChildList();
          
            //System.out.println("Album track list size " + 
            //                               albumTrackList.size() );
            int m;
            for( m = 0 ; m < albumTrackList.size() ; m++ )
            {
              CDSMusicTrack tmpTrack = (CDSMusicTrack)albumTrackList.get(m);
              
              if( origTrackNum <= tmpTrack.getOriginalTrackNum() )
                break;
            }
            albumObj.insertChildAt( track, m );
          }
        }
        else
        {
          CDSMusicAlbum albumObj = new CDSMusicAlbum();
          albumObj.setTitle( track.getAlbum() );
          albumObj.setGenre( track.getGenre() );
          albumObj.addChild( track );
          
          albumHashMap.put(hashKey, albumObj);
        }
        
      }
    }

    // Now shuffle the albums. Need to make a list out of the hash table
    // entries to use the Collections shuffle routine

    ArrayList albumList = new ArrayList();

    Iterator it = albumHashMap.values().iterator();
    while (it.hasNext())
    {
      CDSMusicAlbum albumObj = (CDSMusicAlbum)it.next();
      albumList.add( albumObj );
    }
    
    Collections.shuffle( albumList );
    
    //
    // Clear this list and re-add tracks from shuffled map
    //
    clear();
    
    for( int n = 0 ; n < albumList.size() ; n++ )
    {
      CDSMusicAlbum albumObj = (CDSMusicAlbum)albumList.get(n);
      
      int nTracks = albumObj.getChildCount();
      for( int m = 0 ; m < nTracks ; m++ )
      {
        add( albumObj.getChild(m) );
      }
    }
  }


  public String toString()
  {
    StringBuffer buf = new StringBuffer();
    
    buf.append("<DIDL-Lite xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\">\n");
    

    // Set up filter object to output *all* properties (required and optional)
    CDSFilter filter = new CDSFilter("*");

    for( int n = 0 ; n < size() ; n++ )
    {
      CDSObject obj = (CDSObject)get(n);

      buf.append( obj.toXML( filter ) );
      buf.append( "\n" );
    }

    buf.append("</DIDL-Lite>\n");

    return buf.toString();
  }


  /** 
   *  Test program to parse a DIDL (.xml) file and print out the UPNP
   *  ContentDirectory Objects found 
   */ 
  public static void main( String[] args )
  {
    CDSObjectList objList = null;
    try {

        objList = new CDSObjectList( new FileInputStream(args[0]) );

        System.out.println( objList.toString() );

        System.out.println( "\n AFTER ALBUM SHUFFLE \n" );

        objList.shuffleByAlbum();
        
        System.out.println( objList.toString() );

    } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
  }


}
